/*
    Bandleader
    Copyright (c) 2006 by Grant Yoshida

    Licensed under the GPL version 2 or later,
    please see COPYING for details.
*/

#include "ChordView.h"
//
#include "ChangeChord.h"
#include "DeleteBar.h"
#include "DeleteRow.h"
#include "InsertBar.h"
#include "InsertBarAfter.h"
#include "Document.h"
#include "ListSlicer.h"
#include "OpenRow.h"
//
#include "MidiPlayer.h"
//
#include <QtGui/QPainter>
#include <QtGui/QKeyEvent>
#include <QtGui/QMenu>
#include <QtGui/QMessageBox>
#include <QtGui/QUndoStack>
#include <QtCore/QDir>
#include <QtCore/QPoint>
#include <QtCore/QTemporaryFile>
#include <QtCore/QTimer>
//
#include <QtGui/QApplication>
#include <QtGui/QClipboard>
#include <QtCore/QProcess>
//
#include <QtGui/QFileDialog>
//
#include <cmath>
#include <stdexcept>
#include <iostream>



ChordView::ChordView(const DocumentPtr &document, QWidget *parent) :
    QWidget(parent),
    m_document(document),
    m_undoStack(0),
    m_midiPlayer(new MidiPlayer(this)),
    m_midiFileName(),
    m_cursor(this),
    m_playCursor(this),
    m_inputText(),
    m_editingText(false),
    m_inputCellIndex(0),
    m_selectionSet(),
    m_divisionsPerCell(4), // default is 4/4
    m_cellsPerRow(2),
    m_columnsPerRow(m_divisionsPerCell * m_cellsPerRow),
    m_cellWidth(0),
    m_cellHeight(0),
    m_divisionWidth(0),
    m_divisionHeight(0)
{
    m_undoStack = new QUndoStack(this);

    connect(&m_cursor, SIGNAL(cursorMoved()),
            this, SLOT(update()));

    connect(&m_playCursor, SIGNAL(cursorMoved()),
            this, SLOT(update()));

    connect(m_midiPlayer->playTimer(), SIGNAL(timeout()),
            this, SLOT(updatePlayCursor()));

    connect(m_midiPlayer, SIGNAL(stopped()),
            this, SLOT(handleStoppedPlaying()));

    m_selectionSet.push_back(QPoint(5, 0));
    m_selectionSet.push_back(QPoint(6, 0));

    setBackgroundColor();

    setFocusPolicy(Qt::ClickFocus);
}

DocumentPtr ChordView::getDocument() const
{
    return m_document;
}

void ChordView::paintEvent(QPaintEvent *)
{
    QPainter painter(this);

    computeCellDimensions(painter);

    drawSelection(painter);
    drawCursor(painter);
    drawPlayCursor(painter);
    drawChords(painter);
    drawCommandLine(painter);
}

void ChordView::keyPressEvent(QKeyEvent *event)
{
    Qt::KeyboardModifiers modifiers = event->modifiers();

    if (modifiers & Qt::ControlModifier) {
        switch (event->key()) {
            case Qt::Key_Left:
                wordLeft();
                break;
            case Qt::Key_Right:
                wordRight();
                break;
            case Qt::Key_C:
                if (m_editingText)
                    endInput();
                else
                    copy();
                break;
            case Qt::Key_R:
                m_undoStack->redo();
                update();
                break;
            case Qt::Key_V:
                paste();
                break;
            case Qt::Key_X:
                cut();
                break;
        }
    }
    else {

        switch (event->key()) {
            case Qt::Key_H:
            case Qt::Key_Left:
                moveCursorLeft();
                break;
            case Qt::Key_Return:
            case Qt::Key_L:
            case Qt::Key_Right:
                moveCursorRight();
                break;
            case Qt::Key_K:
            case Qt::Key_Up:
                moveCursorUp();
                break;
            case Qt::Key_J:
            case Qt::Key_Down:
                moveCursorDown();
                break;
            case Qt::Key_Escape:
                endInput();
                break;
            case Qt::Key_Backspace:
                backspace();
                break;
            case Qt::Key_Delete:
                if (m_editingText)
                    ;
                else
                    deleteChordUnderCursor();
                break;
            case Qt::Key_Slash:
                if (!m_inputText.isEmpty())
                    appendInput("/");
                break;
            case Qt::Key_1:
                if (!m_inputText.isEmpty())
                    appendInput("1");
                break;
            case Qt::Key_2:
                if (!m_inputText.isEmpty())
                    appendInput("2");
                break;
            case Qt::Key_3:
                if (!m_inputText.isEmpty()) {
                    if (m_inputText.endsWith("1"))
                        appendInput("3");
                    else
                        appendInput("#");
                }
                break;
            case Qt::Key_4:
                if (!m_inputText.isEmpty())
                    appendInput("4");
                break;
            case Qt::Key_5:
                if (!m_inputText.isEmpty())
                    appendInput("5");
                break;
            case Qt::Key_6:
                if (!m_inputText.isEmpty())
                    appendInput("6");
                break;
            case Qt::Key_7:
                if (!m_inputText.isEmpty())
                    appendInput("7");
                break;
            case Qt::Key_9:
                if (!m_inputText.isEmpty())
                    appendInput("9");
                break;
            case Qt::Key_A:
                if (m_inputText.isEmpty() || m_inputText.endsWith("/"))
                    appendInput("A");
                else
                    appendInput("aug");
                break;
            case Qt::Key_B:
                if (m_inputText.isEmpty() || m_inputText.endsWith("/"))
                    appendInput("B");
                else
                    appendInput("b");
                break;
            case Qt::Key_C:
                appendInput("C");
                break;
            case Qt::Key_D:
                if (m_inputText.isEmpty() || m_inputText.endsWith("/"))
                    appendInput("D");
                else
                    appendInput("dim");
                break;
            case Qt::Key_E:
                appendInput("E");
                break;
            case Qt::Key_F:
                appendInput("F");
                break;
            case Qt::Key_G:
                appendInput("G");
                break;
            case Qt::Key_M:
                if (!m_inputText.isEmpty()) {
                    if (modifiers & Qt::ShiftModifier) {
                        appendInput("maj");
                    }
                    else if (m_inputText.endsWith("m")) {
                        backspace();
                        appendInput("maj");
                    }
                    else
                        appendInput("m");
                }
                break;
            case Qt::Key_I:
            case Qt::Key_Insert:
                if (modifiers & Qt::ShiftModifier)
                    insertBar();
                else
                    insertBarAfter();
                break;
            case Qt::Key_O:
                openRow();
                break;
            case Qt::Key_P:
                paste();
                break;
            case Qt::Key_S:
                if (!m_inputText.isEmpty()) {
                    appendInput("sus");
                }
                break;
            case Qt::Key_U:
                m_undoStack->undo();
                update();
                break;
            case Qt::Key_V:
                wordLeft();
                break;
            case Qt::Key_W:
                wordRight();
                break;
            case Qt::Key_X:
                if (!m_editingText) {
                    if (modifiers & Qt::ShiftModifier)
                        deleteRow();
                    else
                        deleteChordUnderCursor();
                }
                break;
            case Qt::Key_Y:
                copy();
                break;
        }
    }
}

void ChordView::mousePressEvent(QMouseEvent *event)
{
    int col = event->x() / m_divisionWidth;
    int row = event->y() / m_cellHeight;

    m_cursor.setPos(row, col);
}

void ChordView::focusInEvent(QFocusEvent *event)
{
    m_undoStack->setActive();
    QWidget::focusInEvent(event);
}

void ChordView::contextMenuEvent(QContextMenuEvent *event)
{
    QMenu menu(this);
    menu.addAction("Cu&t", this, SLOT(cut()));
    menu.addAction("&Copy", this, SLOT(copy()));
    menu.addAction("&Paste", this, SLOT(paste()));
    menu.addAction("&Delete", this, SLOT(deleteSelection()));
    menu.addSeparator();
    menu.addAction("&Insert bar", this, SLOT(insertBar()));
    menu.addAction("Delete &bar", this, SLOT(deleteBar()));
    menu.exec(event->globalPos());
}

void ChordView::moveCursorLeft()
{
    commitInput();
    m_cursor.moveLeft();
}

void ChordView::moveCursorRight()
{
    commitInput();
    m_cursor.moveRight();
}

void ChordView::moveCursorUp()
{
    commitInput();
    m_cursor.moveUp();
}

void ChordView::moveCursorDown()
{
    commitInput();
    m_cursor.moveDown();
}

void ChordView::setCursorPosition(int index)
{
    int row = rowOfIndex(index);
    int col = colOfIndex(index);

    m_cursor.setPos(row, col);
}


void ChordView::wordLeft()
{
    commitInput();
    m_cursor.moveLeft();

    while (m_cursor.canMoveLeft() && m_cursor.col() % m_divisionsPerCell != 0)
        m_cursor.moveLeft();
}

void ChordView::wordRight()
{
    commitInput();
    m_cursor.moveRight();

    while (m_cursor.canMoveRight() && m_cursor.col() % m_divisionsPerCell != 0)
        m_cursor.moveRight();
}

void ChordView::generateMidiFile()
{
    QTemporaryFile tempMmaFile(QDir::tempPath() + "/XXXXXX.mma");
    m_document->saveAs(tempMmaFile);

    QString mmaFileName(tempMmaFile.fileName());
    tempMmaFile.close();

#ifdef Q_OS_WIN32
    QString mmaPath("c:\\mma\\mma.bat");
#elif defined(Q_OS_MACX)
    QString mmaPath("/Users/gyoshida/Apps/bin/mma");
#else
    QString mmaPath("/data/gyoshida/install/mma-pdf-1.0/mma/mma.py");
#endif

    QProcess mmaProcess;

    mmaProcess.start(mmaPath, QStringList() << mmaFileName);

    if (!mmaProcess.waitForFinished()) {
        throw std::runtime_error(
            "Error invoking MMA.\n"
            "Are you sure you have it installed\n"
            "and it's on your path?");
    }

    QFileInfo info(mmaFileName);
    m_midiFileName = info.absolutePath() + QDir::separator() + info.baseName() + ".mid";
}

void ChordView::togglePlaying()
{
    if (m_midiPlayer->isPlaying()) {
        stop();
    }
    else {
        play();
    }
}

void ChordView::play()
{
    if (!m_midiPlayer->isPlaying()) {
        try {
            generateMidiFile();
            playMidiFile(m_midiFileName);
        }
        catch (std::exception &e) {
            msgBox(e.what());
            stoppedPlaying();
        }
    }
}

void ChordView::stop()
{
    if (m_midiPlayer->isPlaying())
        m_midiPlayer->stop();
}

void ChordView::handleStoppedPlaying()
{
    update();
    stoppedPlaying();
}

void ChordView::playMidiFile(const QString &fileName)
{
    m_midiPlayer->load(fileName);
    m_midiPlayer->play();
    update();
}

void ChordView::startedPlaying()
{
    playingStateChanged(true);
}

void ChordView::stoppedPlaying()
{
    playingStateChanged(false);
}

void ChordView::msgBox(const QString &message)
{
    QMessageBox::information(
        this, 
        "Bandleader",
        message);
}


void ChordView::setChord(int index, const QString &chord)
{
    m_undoStack->push(new ChangeChord(this, index, chord));
}

void ChordView::deleteChord(int index)
{
    m_undoStack->push(new ChangeChord(this, index, ""));
}

void ChordView::insertBar(int index)
{
    m_undoStack->push(new InsertBar(this, index));
}

void ChordView::insertBarAfter(int index)
{
    m_undoStack->push(new InsertBarAfter(this, index));
}

void ChordView::deleteBar(int index)
{
    m_undoStack->push(new DeleteBar(this, index));
}

void ChordView::openRow(int index)
{
    m_undoStack->push(new OpenRow(this, index));
}

void ChordView::deleteRow(int index)
{
    m_undoStack->push(new DeleteRow(this, index));
}



void ChordView::appendInput(const QString &text)
{
    const int longestChordAllowedSize = 8;
    if (m_inputText.size() > longestChordAllowedSize)
        return;

    if (!m_editingText) {
        m_editingText = true;
        m_inputCellIndex = m_cursor.index();
    }

    m_inputText += text;
    update();
}


void ChordView::backspace()
{
    if (m_editingText) {
        if (!m_inputText.isEmpty()) {
            if (m_inputText.endsWith("aug") ||
                m_inputText.endsWith("maj") ||
                m_inputText.endsWith("dim") ||
                m_inputText.endsWith("sus")) {
                m_inputText.chop(3);
            }
            else
                m_inputText.chop(1);

            update();
        }
    }
}


void ChordView::commitInput()
{
    if (m_editingText && !m_inputText.isEmpty()) {
        int index = m_inputCellIndex;

        setChord(index, m_inputText);
    }

    endInput();
}

void ChordView::endInput()
{
    if (m_editingText) {
        m_editingText = false;
        m_inputText = "";

        update();
    }
}


void ChordView::deleteChordUnderCursor()
{
    if (m_document) {
        int index = m_cursor.index();

        if (!m_document->chordAt(index).isEmpty()) {
            deleteChord(index);
            update();
        }
    }
}


void ChordView::insertBar()
{
    insertBar(m_cursor.index());
    update();
}

void ChordView::insertBarAfter()
{
    insertBarAfter(m_cursor.index());
    update();
}

void ChordView::deleteBar()
{
    deleteBar(m_cursor.index());
    update();
}

void ChordView::openRow()
{
    openRow(m_cursor.index());
    update();
}

void ChordView::deleteRow()
{
    deleteRow(m_cursor.index());
    update();
}

void ChordView::copy()
{
    QClipboard *clipboard = QApplication::clipboard();
    clipboard->setText(m_document->chordAt(m_cursor.index()));
}

void ChordView::paste()
{
    QClipboard *clipboard = QApplication::clipboard();
    QString text(clipboard->text());

    if (!text.isEmpty()) {
        setChord(m_cursor.index(), text);
        update();
    }
}

void ChordView::cut()
{
    copy();
    deleteChordUnderCursor();
}

void ChordView::deleteSelection()
{
    deleteChordUnderCursor();
}


void ChordView::save()
{
    QString fileName(m_document->fileName());

    if (fileName.isEmpty())
        saveAs();
    else
        m_document->saveAs(fileName);
}

void ChordView::saveAs()
{
    QFileDialog dialog(this, "Save Document");

    dialog.setAcceptMode(QFileDialog::AcceptSave);
    dialog.setFileMode(QFileDialog::AnyFile);
    dialog.setDirectory(QDir::currentPath());
    dialog.setDefaultSuffix("bandleader");
    dialog.setViewMode(QFileDialog::List);

    QStringList filters;
    filters 
        << "Bandleader Song (*.bandleader)"
        << "MMA Song (*.mma)";
    dialog.setFilters(filters);

    if (dialog.exec()) {
        QString fileName = dialog.selectedFiles().first();

        if (!fileName.isEmpty())
            m_document->saveAs(fileName);
    }
}


void ChordView::drawChords(QPainter &painter)
{
    if (!m_document)
        return;

    painter.setBrush(Qt::NoBrush);
    painter.setPen(Qt::black);

    int row = 0;

    QStringList chords(m_document->chords());
    ListSlicer<QStringList> slicer(chords);

    while (!slicer.empty()) {
        QStringList chordsInRow(slicer.slice(m_columnsPerRow));
        drawRow(painter, row, chordsInRow);

        ++row;
    }
}

void ChordView::drawRow(QPainter &painter, int row, const QStringList &chords)
{
    int y = row * m_cellHeight;

    ListSlicer<QStringList> slicer(chords);

    for (int col = 0; col < m_cellsPerRow; ++col) {
        QStringList chordsInCell(slicer.slice(m_divisionsPerCell));

        if (!chordsInCell.empty()) {
            int x = col * m_cellWidth;

            drawCell(painter, chordsInCell, x, y);
        }
    }
}

void ChordView::drawCell(QPainter &painter, const QStringList &chords, int x, int y)
{
    QRect cell(x, y, m_cellWidth, m_cellHeight);
    painter.drawRect(cell);

    y += m_divisionHeight;

    foreach (QString chord, chords) {
        QString chordText = ' ' + chord;
        painter.drawText(x, y, chordText);
        x += m_divisionWidth;
    }
}

void ChordView::colorCell(QPainter &painter, int row, int col,
                          const QBrush &brush, const QPen &pen)
{
    int x = col * m_divisionWidth;
    int y = row * m_cellHeight;

    painter.setBrush(brush);
    painter.setPen(pen);

    painter.drawRect(x, y, m_divisionWidth, m_cellHeight);
}

void ChordView::drawCursor(QPainter &painter)
{
    QBrush brush(Qt::gray, Qt::Dense6Pattern);
    QPen pen(Qt::gray);

    colorCell(painter, m_cursor.row(), m_cursor.col(), brush, pen);
}

void ChordView::drawPlayCursor(QPainter &painter)
{
    QBrush brush(Qt::red, Qt::Dense4Pattern);
    QPen pen(Qt::NoPen);

    if (m_midiPlayer->isPlaying())
        colorCell(painter, m_playCursor.row(), m_playCursor.col(), brush, pen);
}


void ChordView::drawCommandLine(QPainter &painter)
{
    if (!m_editingText)
        return;

    painter.setPen(Qt::black);

    int x = 0;
    int y = virtualHeight() + 30;

    painter.drawText(x, y, m_inputText);

    // draw caret
    QFontMetrics metrics(painter.fontMetrics());
    x = metrics.width(m_inputText);
    painter.drawLine(x, y - metrics.height(), x, y);
}

void ChordView::drawSelection(QPainter &painter)
{
    QBrush brush(QColor(0, 0, 255, 127));
    QPen pen(Qt::NoPen);

    foreach (QPoint cell, m_selectionSet) {
        colorCell(painter, cell.y(), cell.x(), brush, pen);
    }
}


void ChordView::computeCellDimensions(QPainter &painter)
{
    QFontMetrics metrics(painter.fontMetrics());
    QString longestChordAllowed("F#M13#11");
    m_divisionWidth = metrics.width(longestChordAllowed);
    m_divisionHeight = metrics.height();

    m_cellWidth = m_divisionWidth * m_divisionsPerCell;
    m_cellHeight = m_divisionHeight * 4;
}


void ChordView::setBackgroundColor()
{
    QPalette palette;
    palette.setColor(backgroundRole(), Qt::white);
    setPalette(palette);

    setAutoFillBackground(true);
}


void ChordView::updatePlayCursor()
{
    int index = m_midiPlayer->beatCount();

    int row = rowOfIndex(index);
    int col = colOfIndex(index);

    m_playCursor.setPos(row, col);
}


int ChordView::numCols() const
{
    return m_columnsPerRow;
}

int ChordView::numRows() const
{
    return m_document ?
        (int)ceil((double)m_document->numChords() / m_columnsPerRow) : 0;
}

int ChordView::cellsPerRow() const
{
    return m_cellsPerRow;
}

QUndoStack *ChordView::undoStack() const
{
    return m_undoStack;
}


int ChordView::indexOfRow(int row) const
{
    return row * numCols();
}

int ChordView::indexOf(int row, int col) const
{
    return row * numCols() + col;
}


int ChordView::rowOfIndex(int index) const
{
    return index / m_columnsPerRow;
}

int ChordView::colOfIndex(int index) const
{
    return index % m_columnsPerRow;
}


int ChordView::virtualWidth() const
{
    return m_columnsPerRow * m_divisionWidth;
}

int ChordView::virtualHeight() const
{
    return m_cellHeight * numRows();
}

QSize ChordView::sizeHint() const
{
    return QSize(virtualWidth(), virtualHeight());
}
